// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#nullable enable

using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.CompilerServices;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;

/// <summary>
/// A default <see cref="ModelMetadata"/> implementation.
/// </summary>
public class DefaultModelMetadata : ModelMetadata
{
    private readonly IModelMetadataProvider _provider;
    private readonly ICompositeMetadataDetailsProvider _detailsProvider;
    private readonly DefaultMetadataDetails _details;

    // Default message provider for all DefaultModelMetadata instances; cloned before exposing to
    // IBindingMetadataProvider instances to ensure customizations are not accidentally shared.
    private readonly DefaultModelBindingMessageProvider _modelBindingMessageProvider;

    private ReadOnlyDictionary<object, object>? _additionalValues;
    private ModelMetadata? _elementMetadata;
    private ModelMetadata? _constructorMetadata;
    private bool? _isBindingRequired;
    private bool? _isReadOnly;
    private bool? _isRequired;
    private ModelPropertyCollection? _properties;
    private bool? _validateChildren;
    private bool? _hasValidators;
    private ReadOnlyCollection<object>? _validatorMetadata;

    /// <summary>
    /// Creates a new <see cref="DefaultModelMetadata"/>.
    /// </summary>
    /// <param name="provider">The <see cref="IModelMetadataProvider"/>.</param>
    /// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param>
    /// <param name="details">The <see cref="DefaultMetadataDetails"/>.</param>
    public DefaultModelMetadata(
        IModelMetadataProvider provider,
        ICompositeMetadataDetailsProvider detailsProvider,
        DefaultMetadataDetails details)
        : this(provider, detailsProvider, details, new DefaultModelBindingMessageProvider())
    {
    }

    /// <summary>
    /// Creates a new <see cref="DefaultModelMetadata"/>.
    /// </summary>
    /// <param name="provider">The <see cref="IModelMetadataProvider"/>.</param>
    /// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param>
    /// <param name="details">The <see cref="DefaultMetadataDetails"/>.</param>
    /// <param name="modelBindingMessageProvider">The <see cref="Metadata.DefaultModelBindingMessageProvider"/>.</param>
    public DefaultModelMetadata(
        IModelMetadataProvider provider,
        ICompositeMetadataDetailsProvider detailsProvider,
        DefaultMetadataDetails details,
        DefaultModelBindingMessageProvider modelBindingMessageProvider)
        : base(details.Key)
    {
        ArgumentNullException.ThrowIfNull(provider);
        ArgumentNullException.ThrowIfNull(detailsProvider);
        ArgumentNullException.ThrowIfNull(details);
        ArgumentNullException.ThrowIfNull(modelBindingMessageProvider);

        _provider = provider;
        _detailsProvider = detailsProvider;
        _details = details;
        _modelBindingMessageProvider = modelBindingMessageProvider;
    }

    /// <summary>
    /// Gets the set of attributes for the current instance.
    /// </summary>
    public ModelAttributes Attributes => _details.ModelAttributes;

    /// <inheritdoc />
    public override ModelMetadata? ContainerMetadata => _details.ContainerMetadata;

    /// <summary>
    /// Gets the <see cref="Metadata.BindingMetadata"/> for the current instance.
    /// </summary>
    /// <remarks>
    /// Accessing this property will populate the <see cref="Metadata.BindingMetadata"/> if necessary.
    /// </remarks>
    public BindingMetadata BindingMetadata
    {
        get
        {
            if (_details.BindingMetadata == null)
            {
                var context = new BindingMetadataProviderContext(Identity, _details.ModelAttributes);

                // Provide a unique ModelBindingMessageProvider instance so providers' customizations are per-type.
                context.BindingMetadata.ModelBindingMessageProvider =
                    new DefaultModelBindingMessageProvider(_modelBindingMessageProvider);

                _detailsProvider.CreateBindingMetadata(context);
                _details.BindingMetadata = context.BindingMetadata;
            }

            return _details.BindingMetadata;
        }
    }

    /// <summary>
    /// Gets the <see cref="Metadata.DisplayMetadata"/> for the current instance.
    /// </summary>
    /// <remarks>
    /// Accessing this property will populate the <see cref="Metadata.DisplayMetadata"/> if necessary.
    /// </remarks>
    public DisplayMetadata DisplayMetadata
    {
        get
        {
            if (_details.DisplayMetadata == null)
            {
                var context = new DisplayMetadataProviderContext(Identity, _details.ModelAttributes);
                _detailsProvider.CreateDisplayMetadata(context);
                _details.DisplayMetadata = context.DisplayMetadata;
            }

            return _details.DisplayMetadata;
        }
    }

    /// <summary>
    /// Gets the <see cref="Metadata.ValidationMetadata"/> for the current instance.
    /// </summary>
    /// <remarks>
    /// Accessing this property will populate the <see cref="Metadata.ValidationMetadata"/> if necessary.
    /// </remarks>
    public ValidationMetadata ValidationMetadata
    {
        get
        {
            if (_details.ValidationMetadata == null)
            {
                var context = new ValidationMetadataProviderContext(Identity, _details.ModelAttributes);
                _detailsProvider.CreateValidationMetadata(context);
                _details.ValidationMetadata = context.ValidationMetadata;
            }

            return _details.ValidationMetadata;
        }
    }

    /// <inheritdoc />
    public override IReadOnlyDictionary<object, object> AdditionalValues
    {
        get
        {
            if (_additionalValues == null)
            {
                _additionalValues = new ReadOnlyDictionary<object, object>(DisplayMetadata.AdditionalValues);
            }

            return _additionalValues;
        }
    }

    /// <inheritdoc />
    public override BindingSource? BindingSource => BindingMetadata.BindingSource;

    /// <inheritdoc />
    public override string? BinderModelName => BindingMetadata.BinderModelName;

    /// <inheritdoc />
    public override Type? BinderType => BindingMetadata.BinderType;

    /// <inheritdoc />
    public override bool ConvertEmptyStringToNull => DisplayMetadata.ConvertEmptyStringToNull;

    /// <inheritdoc />
    public override string? DataTypeName => DisplayMetadata.DataTypeName;

    /// <inheritdoc />
    public override string? Description
    {
        get
        {
            if (DisplayMetadata.Description == null)
            {
                return null;
            }

            return DisplayMetadata.Description();
        }
    }

    /// <inheritdoc />
    public override string? DisplayFormatString => DisplayMetadata.DisplayFormatStringProvider();

    /// <inheritdoc />
    public override string? DisplayName
    {
        get
        {
            if (DisplayMetadata.DisplayName == null)
            {
                return null;
            }

            return DisplayMetadata.DisplayName();
        }
    }

    /// <inheritdoc />
    public override string? EditFormatString => DisplayMetadata.EditFormatStringProvider();

    /// <inheritdoc />
    public override ModelMetadata? ElementMetadata
    {
        get
        {
            if (_elementMetadata == null && ElementType != null)
            {
                _elementMetadata = _provider.GetMetadataForType(ElementType);
            }

            return _elementMetadata;
        }
    }

    /// <inheritdoc />
    public override IEnumerable<KeyValuePair<EnumGroupAndName, string>>? EnumGroupedDisplayNamesAndValues
        => DisplayMetadata.EnumGroupedDisplayNamesAndValues;

    /// <inheritdoc />
    public override IReadOnlyDictionary<string, string>? EnumNamesAndValues => DisplayMetadata.EnumNamesAndValues;

    /// <inheritdoc />
    public override bool HasNonDefaultEditFormat => DisplayMetadata.HasNonDefaultEditFormat;

    /// <inheritdoc />
    public override bool HideSurroundingHtml => DisplayMetadata.HideSurroundingHtml;

    /// <inheritdoc />
    public override bool HtmlEncode => DisplayMetadata.HtmlEncode;

    /// <inheritdoc />
    public override bool IsBindingAllowed
    {
        get
        {
            if (MetadataKind == ModelMetadataKind.Type)
            {
                return true;
            }
            else
            {
                return BindingMetadata.IsBindingAllowed;
            }
        }
    }

    /// <inheritdoc />
    public override bool IsBindingRequired
    {
        get
        {
            if (!_isBindingRequired.HasValue)
            {
                if (MetadataKind == ModelMetadataKind.Type)
                {
                    _isBindingRequired = false;
                }
                else
                {
                    _isBindingRequired = BindingMetadata.IsBindingRequired;
                }
            }

            return _isBindingRequired.Value;
        }
    }

    /// <inheritdoc />
    public override bool IsEnum => DisplayMetadata.IsEnum;

    /// <inheritdoc />
    public override bool IsFlagsEnum => DisplayMetadata.IsFlagsEnum;

    /// <inheritdoc />
    public override bool IsReadOnly
    {
        get
        {
            if (!_isReadOnly.HasValue)
            {
                if (MetadataKind == ModelMetadataKind.Type)
                {
                    _isReadOnly = false;
                }
                else if (BindingMetadata.IsReadOnly.HasValue)
                {
                    _isReadOnly = BindingMetadata.IsReadOnly;
                }
                else
                {
                    _isReadOnly = _details.PropertySetter == null;
                }
            }

            return _isReadOnly.Value;
        }
    }

    /// <inheritdoc />
    public override bool IsRequired
    {
        get
        {
            if (!_isRequired.HasValue)
            {
                if (ValidationMetadata.IsRequired.HasValue)
                {
                    _isRequired = ValidationMetadata.IsRequired;
                }
                else
                {
                    // Default to IsRequired = true for non-Nullable<T> value types.
                    _isRequired = !IsReferenceOrNullableType;
                }
            }

            return _isRequired.Value;
        }
    }

    /// <inheritdoc />
    public override ModelBindingMessageProvider ModelBindingMessageProvider =>
        BindingMetadata.ModelBindingMessageProvider!;

    /// <inheritdoc />
    public override string? NullDisplayText => DisplayMetadata.NullDisplayTextProvider();

    /// <inheritdoc />
    public override int Order => DisplayMetadata.Order;

    /// <inheritdoc />
    public override string? Placeholder
    {
        get
        {
            if (DisplayMetadata.Placeholder == null)
            {
                return null;
            }

            return DisplayMetadata.Placeholder();
        }
    }

    /// <inheritdoc />
    public override ModelPropertyCollection Properties
    {
        get
        {
            if (_properties == null)
            {
                var properties = _provider.GetMetadataForProperties(ModelType);
                properties = properties.OrderBy(p => p.Order);
                _properties = new ModelPropertyCollection(properties);
            }

            return _properties;
        }
    }

    /// <inheritdoc />
    public override ModelMetadata? BoundConstructor
    {
        get
        {
            if (BindingMetadata.BoundConstructor == null)
            {
                return null;
            }

            if (_constructorMetadata == null)
            {
                var modelMetadataProvider = (ModelMetadataProvider)_provider;
                _constructorMetadata = modelMetadataProvider.GetMetadataForConstructor(BindingMetadata.BoundConstructor, ModelType);
            }

            return _constructorMetadata;
        }
    }

    /// <inheritdoc/>
    public override IReadOnlyList<ModelMetadata>? BoundConstructorParameters => _details.BoundConstructorParameters;

    /// <inheritdoc />
    public override IPropertyFilterProvider? PropertyFilterProvider => BindingMetadata.PropertyFilterProvider;

    /// <inheritdoc />
    public override bool ShowForDisplay => DisplayMetadata.ShowForDisplay;

    /// <inheritdoc />
    public override bool ShowForEdit => DisplayMetadata.ShowForEdit;

    /// <inheritdoc />
    public override string? SimpleDisplayProperty => DisplayMetadata.SimpleDisplayProperty;

    /// <inheritdoc />
    public override string? TemplateHint => DisplayMetadata.TemplateHint;

    /// <inheritdoc />
    public override IPropertyValidationFilter? PropertyValidationFilter => ValidationMetadata.PropertyValidationFilter;

    /// <inheritdoc />
    public override bool ValidateChildren
    {
        get
        {
            if (!_validateChildren.HasValue)
            {
                if (ValidationMetadata.ValidateChildren.HasValue)
                {
                    _validateChildren = ValidationMetadata.ValidateChildren.Value;
                }
                else if (IsComplexType || IsEnumerableType)
                {
                    _validateChildren = true;
                }
                else
                {
                    _validateChildren = false;
                }
            }

            return _validateChildren.Value;
        }
    }

    /// <inheritdoc />
    public override bool? HasValidators
    {
        get
        {
            if (!_hasValidators.HasValue)
            {
                var visited = new HashSet<DefaultModelMetadata>();

                _hasValidators = CalculateHasValidators(visited, this);
            }

            return _hasValidators.Value;
        }
    }

    internal override bool PropertyHasValidators => ValidationMetadata.PropertyHasValidators;

    /// <inheritdoc />
    internal override string? ValidationModelName => ValidationMetadata.ValidationModelName;

    internal static bool CalculateHasValidators(HashSet<DefaultModelMetadata> visited, ModelMetadata metadata)
    {
        RuntimeHelpers.EnsureSufficientExecutionStack();

        if (metadata?.GetType() != typeof(DefaultModelMetadata))
        {
            // The calculation is valid only for DefaultModelMetadata instances. Null, other ModelMetadata instances
            // or subtypes of DefaultModelMetadata will be treated as always requiring validation.
            return true;
        }

        var defaultModelMetadata = (DefaultModelMetadata)metadata;

        if (defaultModelMetadata._hasValidators.HasValue)
        {
            // Return a previously calculated value if available.
            return defaultModelMetadata._hasValidators.Value;
        }

        if (defaultModelMetadata.ValidationMetadata.HasValidators != false)
        {
            // Either the ModelMetadata instance has some validators (HasValidators = true) or it is non-deterministic (HasValidators = null).
            // In either case, assume it has validators.
            return true;
        }

        // Before inspecting properties or elements of a collection, ensure we do not have a cycle.
        // Consider a model like so
        //
        // Employee { BusinessDivision Division; int Id; string Name; }
        // BusinessDivision { int Id; List<Employee> Employees }
        //
        // If we get to the Employee element from Employee.Division.Employees, we can return false for that instance
        // and allow other properties of BusinessDivision and Employee to determine if it has validators.
        if (!visited.Add(defaultModelMetadata))
        {
            return false;
        }

        // We have inspected the current element. Inspect properties or elements that may contribute to this value.
        if (defaultModelMetadata.IsEnumerableType)
        {
            if (CalculateHasValidators(visited, defaultModelMetadata.ElementMetadata!))
            {
                return true;
            }
        }
        else if (defaultModelMetadata.IsComplexType)
        {
            var parameters = defaultModelMetadata.BoundConstructor?.BoundConstructorParameters ?? Array.Empty<ModelMetadata>();
            foreach (var parameter in parameters)
            {
                if (CalculateHasValidators(visited, parameter))
                {
                    return true;
                }
            }

            foreach (var property in defaultModelMetadata.BoundProperties)
            {
                if (CalculateHasValidators(visited, property))
                {
                    return true;
                }
            }
        }

        // We've come this far. The ModelMetadata does not have any validation
        return false;
    }

    /// <inheritdoc />
    public override IReadOnlyList<object> ValidatorMetadata
    {
        get
        {
            if (_validatorMetadata == null)
            {
                _validatorMetadata = new ReadOnlyCollection<object>(ValidationMetadata.ValidatorMetadata);
            }

            return _validatorMetadata;
        }
    }

    /// <inheritdoc />
    public override Func<object, object?>? PropertyGetter => _details.PropertyGetter;

    /// <inheritdoc />
    public override Action<object, object?>? PropertySetter => _details.PropertySetter;

    /// <inheritdoc/>
    public override Func<object?[], object>? BoundConstructorInvoker => _details.BoundConstructorInvoker;

    internal DefaultMetadataDetails Details => _details;

    /// <inheritdoc />
    public override ModelMetadata GetMetadataForType(Type modelType)
    {
        return _provider.GetMetadataForType(modelType);
    }

    /// <inheritdoc />
    public override IEnumerable<ModelMetadata> GetMetadataForProperties(Type modelType)
    {
        return _provider.GetMetadataForProperties(modelType);
    }
}
